//
//  DPTestCase.m
//  DPUnitTest
//
//  Created by Ofri Wolfus on 06/10/06.
//  Copyright 2006 Ofri Wolfus. All rights reserved.
//

#import "DPTestCase-Private.h"
#import "DPObjCRuntime.h"
#import "DPTestStatistics-Private.h"
#import "DPTestsModule.h"
#import "DPUtilities.h"
#import "DPUIElement.h"
#import "DPAccessibilityNotificationCenter.h"
#import <ExceptionHandling/NSExceptionHandler.h>
#include <libkern/OSAtomic.h>


typedef BOOL (* dp_BOOL_msgSend)(id self, SEL op, ...);


@interface DPTestCase (implementation_private)
/*
 * @abstract Runs a specific test and returns a statistics object with the results.
 * @discussion You rarely need to invoke this method directly.
 */
- (DPTestStatistics *)runTestNamed:(NSString *)testName;

- (void)checkStatus:(DPTestStatistics *)stats;
@end


@implementation DPTestCase

static CFRunLoopRef _mainRunLoop = NULL;

void __attribute__((constructor)) _DPStoreMainRL(void) {
	_mainRunLoop = (CFRunLoopRef)CFRetain(CFRunLoopGetCurrent());
}

- (id)init {
	if ((self = [super init])) {
		unsigned int count, i;
		Method *m = class_copyMethodList([self class], &count);
		
		_refCount = 1;
		_tests = [[NSMutableArray allocWithZone:[self zone]] initWithCapacity:count];
		_nextTest = 0U;
		_pendingTests = [[NSMutableDictionary allocWithZone:[self zone]] init];
		_module = [[DPTestsModule moduleAtPath:[[NSBundle bundleForClass:[self class]] bundlePath]] retain];
		_completed = NO;
		
		// Find all of our tests
		for (i = 0; i < count; i++) {
			NSString *name = NSStringFromSelector(method_getName(m[i]));
			
			if ([name hasPrefix:@"test"])
				[_tests addObject:name];
		}
		
		// Sort tests alphabetically
		//[_tests sortUsingSelector:@selector(compare:)];
	}
	
	return self;
}

- (void)dealloc {
	[[DPAccessibilityNotificationCenter defaultCenter] removeObserver:self];
	[_tests release]; _tests = nil;
	[_pendingTests release]; _pendingTests = nil;
	[_module release]; _module = nil;
	[super dealloc];
}

- (DPTestStatus)verifyResult:(id)result ofTest:(NSString *)testName {
	return DPTestPending;
}

- (unsigned)numberOfIterations {
	return 1U;
}

- (BOOL)verifiesAllTests {
	return NO;
}

- (DPTestStatistics *)nextTest {
	DPTestStatistics *sts = nil;
	
	if (_nextTest < [_tests count]) {
		sts = [self runTestNamed:[_tests objectAtIndex:_nextTest]];
		++_nextTest;
	}
	
	// Be sure to notify everyone when we're done
	// HACK: See the comment in -checkStatus: for explanation of the
	// reasoning for passing the statistics object (!).
	[self checkStatus:sts && [sts status] == DPTestPending ? sts : nil];
	
	return sts;
}

- (DPTestStatistics *)runTestNamed:(NSString *)testName {
	DPTestStatistics	*stats = [_pendingTests objectForKey:testName] ?: [[DPTestStatistics new] autorelease];
	CFAbsoluteTime		start, total = 0;
	unsigned			i, iterCount = [self numberOfIterations];
	SEL					sel = NSSelectorFromString(testName);
	char				retType = *([[self methodSignatureForSelector:sel] methodReturnType]);
	id					retVal = nil;
	DPTestStatus		result = YES;
	NSString			*verificationName = [NSString stringWithFormat:@"verifyTest%@:",[testName substringFromIndex:4U]];
	SEL					verSel = NSSelectorFromString(verificationName);
	Method				verMeth = class_getInstanceMethod([self class], verSel);
	BOOL				verifyAll = [self verifiesAllTests];
	NSAutoreleasePool	*pool = nil;
	
	// Set some already known info
	[stats setTestName:testName];
	[stats setCaseName:[self className]];
	[stats setModule:[self module]];
	
	
	// Wrap the execution of the test in an exception handler
	// as failed tests throw exceptions.
	@try {
		// Execute our test as many times as needed as long as it passes
		for (i = 0; i < iterCount && result == YES; i++) {
			//if (i % 10 == 0)
			pool = [[NSAutoreleasePool alloc] init];
			// This is when we start testing
			start = CFAbsoluteTimeGetCurrent();
			
			// Call the test method
			switch (retType) {
				// id, Class and void, all go throw objc_msgSend,
				// but void returns nothing.
				case _C_ID:
				case _C_CLASS:
					retVal = objc_msgSend(self, sel);
					break;
					
				case _C_VOID:
					objc_msgSend(self, sel);
					break;
					
				// BOOL return needs special casting (well not really,
				// but that's The Right Way (TM)).
				// Not casting to BOOL also allows us to accept DPTestStatus
				// return values.
				default:
					result = ((dp_BOOL_msgSend)objc_msgSend)(self, sel);
					break;
			}
			
			// After the test finished, we get the current time
			// and calculate the period it took for the test to complete.
			total += CFAbsoluteTimeGetCurrent() - start;
			
			// First of all, the first test is always verified.
			// After it, it depends on the return value of -verifiesAllTests
			// (that is stored in verifyAll).
			if (verifyAll || i == 0) {
				// Methods returning id, Class or void need verifications
				if (retType == _C_ID || retType == _C_CLASS || retType == _C_VOID) {
					if (verMeth)
						result = ((dp_BOOL_msgSend)method_getImplementation(verMeth))(self, sel, retVal);
					else
						result = [self verifyResult:retVal ofTest:testName];
					
					// Stop at pending tests, and store them for a later verification
					if (result == DPTestPending) {
						[_pendingTests setValue:stats
										 forKey:testName];
						[pool release]; pool = nil;
						break;
						
					}
				}
			}
			
			//if (i % 10 == 0)
			[pool release]; pool = nil;
			
			// If the test failed, we'll throw an exception that'll be caught by the
			// following catch block.
			if (result == DPTestFailed) {
				NSString *reason = [NSString stringWithFormat:@"Test %@ of case %@ failed.", testName,
					[self className]];
				
				@throw [NSException exceptionWithName:DPFailedTestException
											   reason:reason
											 userInfo:nil];
			}
		}
		
		// Update our counter
		[stats setNumberOfIterations:[stats numberOfIterations] + i];
	
	// Catch exceptions thrown by failed tests
	} @catch (NSException *exep) {
		[pool release]; pool = nil;
		NSDictionary *uInfo = [exep userInfo];
		NSMutableDictionary *info = uInfo ? [NSMutableDictionary dictionaryWithDictionary:uInfo] : [NSMutableDictionary dictionary];
		NSString *reason = [exep reason];
		
		// Obviously, something went wrong...
		result = DPTestFailed;
		
		// If someone catches this exception, let him also get the statistics
		[info setValue:stats
				forKey:DPTestStatisticsKey];
		// A stack trace might be useful so copy it if one is found
		// (re-thowing the exception should override the trace)
		[info setValue:[info valueForKey:NSStackTraceKey]
				forKey:@"DPStackTrace"];
		
		// Set the status and return value of our statistics object
		// (needed since notifications are processed immediately, meaning
		// observers will be informed before the call to postNotificationName: returns).
		[stats setStatus:result];
		//[stats setReturnValue:retVal];
		[stats setExecTime:total / (i + 1)];
		
		// Set the fail reason of our statistics object
		[stats setFailReason:reason];
		
		// Post a failure notification
		[[NSNotificationCenter defaultCenter] postNotificationName:DPTestFailedNotification
															object:self
														  userInfo:[NSDictionary dictionaryWithObject:stats
																							   forKey:DPTestStatisticsKey]];
		
		// Throw a new exception with our new info
		@throw [NSException exceptionWithName:[exep name]
									   reason:reason
									 userInfo:info];
		
	}
	
	// Apply the info to our statistics object
	[stats setStatus:result];
	//[stats setReturnValue:retVal];
	[stats setExecTime:total / (i + 1)];
	
	return stats;
}

- (void)checkStatus:(DPTestStatistics *)stats {
	if (!_completed && _nextTest >= [_tests count] && [_pendingTests count] == 0U) {
		/*
		 * HACK: An ugly hack!
		 * Since this method is called before -nextTest returns, the completion notification
		 * is also posted before -nextTest returns, so DPTestsModule will be missing
		 * the statistics object of the last test.
		 * In order to work around this without completely changing the API, we include the
		 * statistics with our notification.
		 * NOTE: Statistics will be passed only for syncronized tests, and not for async tests
		 * that just got verified, or we'll end up with the same statistics object twice.
		 */
		[[NSNotificationCenter defaultCenter] postNotificationName:DPTestCaseCompletedNotification
															object:self
														  userInfo:stats ? [NSDictionary dictionaryWithObject:stats
																									   forKey:DPTestStatisticsKey]
																		 : nil];
		_completed = YES;
	}
}

- (BOOL)isValid {
	return _completed;
}

/*
 * This is the real implementation of -setStatus:forPendingTest:
 * that is not thread safe and therefor must always be invoked in the main
 * thread.
 * In order to assure this, -setStatus:forPendingTest: uses
 * performSelectorOnMainThread to invoke _setStatusForPendingTest:
 * (it only allows one argument to be passed) which then invokes this implementation.
 * TODO: Use DPMessagesQueue from DPFoundation in order to do this The Right Way(TM).
 */
- (void)_setStatus:(DPTestStatus)st forPendingTest:(NSString *)testName {
	DPTestStatistics *statistics = [_pendingTests valueForKey:testName];
	unsigned itr = [statistics numberOfIterations];
	
	// Make sure no one tries to fool us
	if (st == DPTestPending)
		return;
	
	// In case of async tests, the iterations over the test
	// must be implicit and can't be done in a loop like
	// synchronized tests.
	if (st == YES && itr < [self numberOfIterations]) {
		[statistics setNumberOfIterations:itr + 1];
		[self runTestNamed:testName];
		return;
	}
	
	// Set the result of our test
	[statistics setStatus:st];
	
	// Make sure the statistics object stays long enough after we
	// remove it from the pending tests dict, ...
	[[statistics retain] autorelease];
	// ...and actually remove it.
	[_pendingTests removeObjectForKey:testName];
	
	// Post a failure notification
	if (st == DPTestFailed) {
		// Set the fail reason
		// XXX We really need to come up with a better reason.
		[statistics setFailReason:@"Failed after pending"];
		
		[[NSNotificationCenter defaultCenter] postNotificationName:DPTestFailedNotification
															object:self
														  userInfo:[NSDictionary dictionaryWithObject:statistics
																							   forKey:DPTestStatisticsKey]];
	}
	
	// Be sure to notify everyone when we're done
	// HACK: We MUST (!) pass nil to -checkStatus: here.
	// See the comment in -checkStatus: for more info.
	[self checkStatus:nil];
}

- (void)_setStatusForPendingTest:(NSDictionary *)values {
	CFRunLoopWakeUp(_mainRunLoop);
	[self _setStatus:[[values valueForKey:@"status"] intValue]
	  forPendingTest:[values valueForKey:@"name"]];
}

- (void)setStatus:(DPTestStatus)st forPendingTest:(NSString *)testName {
	NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:testName, @"name",
		[NSNumber numberWithInt:st], @"status", nil];
	
	[self performSelectorOnMainThread:@selector(_setStatusForPendingTest:)
						   withObject:dict
						waitUntilDone:YES];
}

- (DPTestsModule *)module {
	return _module;
}

// We need thread-safe ref counting.
// This might actually be faster than NSObject's implementation.
- (id)retain {
	OSAtomicIncrement32Barrier(&_refCount);
	return self;
}

- (void)release {
	if (OSAtomicDecrement32Barrier(&_refCount) == 0)
		[self dealloc];
}

- (unsigned)retainCount {
	OSMemoryBarrier();
	return (unsigned)_refCount;
}

@end

DPUIElement *DPTestSubject = nil;

NSString * const DPFailedTestException = @"DPFailedTestException";
NSString * const DPTestCaseCompletedNotification = @"DPTestCaseCompletedNotification";
NSString * const DPTestFailedNotification = @"DPTestFailedNotification";
